/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.astoolkit.workflow
{
	
	import avmplus.getQualifiedClassName;
	
	import com.astoolkit.workflow.constant.CLOSED_OUTLET;
	import com.astoolkit.workflow.constant.FailurePolicy;
	import com.astoolkit.workflow.constant.Feed;
	import com.astoolkit.workflow.constant.Flow;
	import com.astoolkit.workflow.constant.PIPELINE_OUTLET;
	import com.astoolkit.workflow.constant.TaskStatus;
	import com.astoolkit.workflow.constant.UNDEFINED;
	import com.astoolkit.workflow.util.DataWrapper;
	
	import flash.events.IEventDispatcher;
	import flash.utils.getDefinitionByName;
	import flash.utils.setTimeout;
	
	import mx.binding.utils.BindingUtils;
	import mx.collections.IList;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.rpc.Fault;
	
   	[DefaultProperty("tasks")]
	public class RepeaterWorkflow extends Workflow implements IRepeater
	{

		[Bindable]
		public function set dataProvider( inValue : IList ) : void
		{
			if( status == TaskStatus.RUNNING )
				return;
			_dataProvider = inValue;
		}
		
		public function get dataProvider() : IList
		{
			return _dataProvider;
		}
		
		protected var _clearDataProviderOnComplete : Boolean;
		
		/**
		 * the data source for this repeater's iteration.
		 */
		[Bindable]
		private var _dataProvider : IList;
		
		/**
		 * When the repeater is running this property contains the item from
		 * dataProvider at currentDataIndex
		 */
		[Bindable]
		public var currentData : *;
		
		/**
		 * Utility property. true if currentDataIndex == 0
		 */
		[Bindable]
		public var processingFirst : Boolean;
		
		/**
		 * Utility property. true if currentDataIndex == dataProvider.length -1
		 */
		[Bindable]
		public var processingLast : Boolean;
		
		/**
		 * If not null, the running repeater will put currentData in 
		 * currentDataTarget[ currentDataProperty ]
		 */
		public var currentDataProperty : String;
		
		/**
		 * The target object where the running repeater will put
		 * currentData at property currentDataProperty.
		 * If not set and currentDataProperty is set
		 * the repeater will look for the property
		 * in the repeater itself or on one of its 
		 * workflow ancestors
		 */
		public var currentDataTarget : IEventDispatcher;
		
		/**
		 * @private
		 */
		protected var _currentDataIndex : int = -1;
		
		/**
		 * @private
		 */
		private var _declaredTasks : Array;
		
		public function RepeaterWorkflow()
		{
			super();
			feed = Feed.CURRENT_DATA;
			
			BindingUtils.bindSetter( onIndexChange, this, "currentDataIndex" );
			/*
				for repeater _iterate is always true.
				iteration is controlled by dataProvider 
			*/
			_iterate = true;
		}
		
		override public function set iterate(inIterate:Boolean):void
		{
			//Repeater workflow ignores direct setting to "iterate"
		}
		
		/*
			Setting the utility properties processingFirst and processingLast
		*/
		/**
		 * @private
		 */
		private function onIndexChange( inIndex : int ) : void
		{
			if( !dataProvider )
			{
				processingFirst = processingLast = false;
				return;
			}
			processingFirst = inIndex == 0;
			processingLast = inIndex == dataProvider.length -1;
		}
		
		/**
		 * he current dataProvider index in a running repeater.
		 * Binding is triggered although this property is read-only
		 */ 
		[Bindable]
		public function get currentDataIndex() : int
		{
			return _currentDataIndex;
		}
		
		/**
		 * @private
		 */
		public function set currentDataIndex( inIndex : int ) : void
		{
			//EMPTY SETTER. NEEDED TO ALLOW BINDING
		}
		
		override public function get declaredTasks() : Array
		{
			return _declaredTasks;
		}
		
		/**
		 * the list of tasks this repeater will iterate through
		 */
		override public function set tasks( inTasks : Array ) : void
		{
			_declaredTasks = inTasks;
		}
		
		/**
		 * Execution entry point for the repeater.
		 * You don't have to call this method yourself
		 * unless the repeater is the top workflow.
		 * @param inRetain  set this to true if you're not keeping
		 * 					an external reference to this workflow
		 * 					and you want to prevent it from being
		 *  				garbage collected
		 */
		override public function begin( inRetain : Boolean = false ):void
		{
			_subPipelineData = UNDEFINED;
			if( !dataProvider || dataProvider.length == 0)
			{
				if(pipelineData is IList && pipelineData.length > 0 )
				{
					dataProvider = pipelineData;
					_clearDataProviderOnComplete = true;				
				}
				else
				{
					LOGGER.info( "RepeaterWorkflow \"" + description + "\" completes with no data");
					complete();
					return;
				}
			}
				
			if( _iterate && dataProvider != null && dataProvider.length > 0 )
			{
				try
				{
					produceTasks();
				}
				catch( e : Error )
				{
					fail( e.getStackTrace() );
					return;
				}
			}
			super.begin( inRetain );
		}
		
		/**
		 * @private
		 */
		protected function produceTasks() : void
		{
			_subPipelineData = UNDEFINED;
			_currentDataIndex ++;
			var oldData : Object = currentData;
			currentData = dataProvider.getItemAt( _currentDataIndex );
			
			if( currentDataProperty  )
			{
				if( !currentDataTarget )
				{
					currentDataTarget = this;
					while( !Workflow(currentDataTarget).hasOwnProperty( currentDataProperty ) && Workflow(currentDataTarget).parent )
						currentDataTarget = Workflow( currentDataTarget ).parent;
					if( !Workflow(currentDataTarget).hasOwnProperty( currentDataProperty ) )
						throw new Error( "Cannot find a target for currentDataProperty: '" + currentDataProperty );
				}
				
				if( currentData != null )
				{
					var dataClass : Class = getDefinitionByName( getQualifiedClassName( currentData ) ) as Class;
					if( !( currentDataTarget[ currentDataProperty ] is dataClass ) )
						throw new Error( "Cannot assign currentData of type " + getQualifiedClassName( currentData ) + 
							" to currentDataProperty of type: " + getQualifiedClassName( currentDataTarget[ currentDataProperty ] ) );
				}
				currentDataTarget[ currentDataProperty ] = currentData;
			}
			
/*			dispatchEvent( 
				new PropertyChangeEvent( 
					PropertyChangeEvent.PROPERTY_CHANGE,
					false,
					false,
					PropertyChangeEventKind.UPDATE,
					"currentDataIndex",
					_currentDataIndex - 1,
					_currentDataIndex,
					this ) );
*/			_tasks = [];
			for each( var task : IWorkflowTask in _declaredTasks )
			{
				task.parent = this;
				_tasks.push( task );
			}
		}

		override public function abort():void
		{
			super.abort();
			
		}
		/**
		 * @private
		 */
		override protected function setSubtaskPipelineData( inTask : IWorkflowTask ) : void
		{
			if( _subPipelineData == UNDEFINED )
			{
				if( _feed == Feed.PIPELINE )
				{
					_subPipelineData = pipelineData;
				}
				else if( _feed == Feed.CURRENT_DATA )
				{
					_subPipelineData = currentData;
				}
			}
			
			if( inTask is IPipelineTask && IPipelineTask( inTask ).taskOutput == UNDEFINED )
			{
				if( IPipelineTask( inTask ).inlet != null )
				{
					try
					{
						if( Object( inTask ).hasOwnProperty( IPipelineTask( inTask ).inlet ) )
						{
							if( inTask[ IPipelineTask( inTask ).inlet ] is Function )
							{
								var f : Function = inTask[ IPipelineTask( inTask ).inlet ] as Function; 
								f.apply( inTask, [ _subPipelineData ] );
							}
							else
								inTask[ IPipelineTask( inTask ).inlet ] = _subPipelineData;
						}
					}
					catch( e : Error )
					{
						trace( e.getStackTrace() );
						//TODO: failure management;
					}
				}
				if( _document )
					_document.$data = new DataWrapper(_subPipelineData );
				IPipelineTask( inTask ).taskInput = _subPipelineData;
			}
		}
		
		[Inspectable(defaultValue="currentData", enumeration="pipeline,currentData")]
		override public function set feed( inFeed : String ) : void
		{
			if( inFeed != Feed.PIPELINE && inFeed != Feed.CURRENT_DATA)
			{
				throw new Error(
					"Workflow 'feed' property can only be either 'pipeline' or 'currentData': " +
					"found '" + inFeed + "'" );
			}
			_feed = inFeed;
		}

		
		/**
		 * @private
		 */
		override protected function onSubtaskCompleted( inTask : IWorkflowTask ) : void
		{
			if( inTask == this )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
				return;
			}
			if( _status == TaskStatus.ABORTED )
			{
				complete();
				return;
			}
			_processedTasks.splice( _processedTasks.indexOf( inTask ), 1 );
			
			if( inTask.status != TaskStatus.ABORTED )
			{
				dispatchTaskEvent( WorkflowEvent.COMPLETED, inTask );
				if( !( inTask is IPipelineTask ) || !IPipelineTask( inTask ).ignoreOutput )
				{
					if( inTask is IPipelineTask && context.failedTask != inTask )
					{
						if( IPipelineTask(inTask).outlet == PIPELINE_OUTLET ) 
							_subPipelineData = IPipelineTask( inTask ).taskOutput;
						else if( IPipelineTask(inTask).outlet is String && ( IPipelineTask(inTask).outlet as String ).match( /^\$\w+$/ ) )
						{
							context.variables[ ( IPipelineTask( inTask ).outlet as String).substr(1) ] = IPipelineTask( inTask ).taskOutput;
							IEventDispatcher( _document ).dispatchEvent(
								new PropertyChangeEvent(
									PropertyChangeEvent.PROPERTY_CHANGE,
									false,
									false,
									PropertyChangeEventKind.UPDATE,
									"$",
									Math.random(),
									context.variables
								) );
							
						}
						else if( IPipelineTask(inTask).outlet != CLOSED_OUTLET )
						{
							_subPipelineData = IPipelineTask( inTask ).outlet;
						}
					}
				}
			}
			context.failedTask = null;
            if( _flow == Flow.SERIAL && _nextTaskIndex < _tasks.length )
            {
                runNextTask();
			}
			else
			{
				//checking whether all of the ran tasks completed
		        if( _processedTasks.length == 0 )
		        {
					complete()
		        }
			}
		}
		
		/**
		 * @private
		 */
		override protected function onSubtaskFault( inTask : IWorkflowTask, inMessage : String, inFault : Fault ) : void
		{
			_context.failedTask = inTask;
			if( inTask.failurePolicy == FailurePolicy.ABORT && inTask.status != TaskStatus.ABORTED )
			{
				LOGGER.error( 
					"Task {0} ({1}) failed. Cause:\n{2}\n{3}", 
					inTask.description, getQualifiedClassName( inTask ),
					inMessage,
					(inFault ? inFault.getStackTrace() : "") 
				);
				dispatchTaskEvent( WorkflowEvent.FAULT, inTask, inMessage, inFault );
				if( _parent == null )
				{
					cleanUp();                   
				}
				abort();
			}
			else 
			{
				if( inTask.failurePolicy == FailurePolicy.SUSPEND && inTask.status != TaskStatus.ABORTED )
					inTask.suspend();
				if( inTask.failurePolicy == FailurePolicy.CONTINUE && inTask.status != TaskStatus.ABORTED )
					nextData();
				onSubtaskCompleted( inTask );
			}
		}
		
		override protected function runSubTask(inTask:IWorkflowTask, inNow:Boolean=false):void
		{
			if( _nextTaskIndex == 1 )
			{
				dispatchEvent( 
					new PropertyChangeEvent( 
						PropertyChangeEvent.PROPERTY_CHANGE,
						false,
						false,
						PropertyChangeEventKind.UPDATE,
						"currentDataIndex",
						_currentDataIndex - 1,
						_currentDataIndex,
						this ) );
				dispatchEvent( 
					new PropertyChangeEvent( 
						PropertyChangeEvent.PROPERTY_CHANGE,
						false,
						false,
						PropertyChangeEventKind.UPDATE,
						"currentData",
						null,
						currentData,
						this ) );
			}
			super.runSubTask( inTask, inNow );
		}
		
		/**
		 * @private
		 */
		private function nextData() : void
		{
			_subPipelineData = UNDEFINED;
			var task : IWorkflowTask;
			for each( task in declaredTasks )
			{
				task.prepare();
			}
			produceTasks();
			_nextTaskIndex = 0;
			_processedTasks = [];
		}
		
		/**
		 * tasks should call this function once finished
		 */
		override protected function complete() : void
		{
			Workflow.LOGGER.debug(
				"Task '{0}' task iteration completed with index {1}", description, _currentDataIndex );
			if( !_iterate || !dataProvider || _currentDataIndex == dataProvider.length -1 )
			{
				Workflow.LOGGER.debug(
					"Task '{0}' completed", description );
				if( !_ignoreOutput )
					_pipelineData = _subPipelineData;
				if ( _clearDataProviderOnComplete )
				{
					_clearDataProviderOnComplete = false;
					_dataProvider = null;
					trace( "dataProvider = null on ", description );
				}
				setTimeout( _deferredComplete, 1, _thread );
			}
			else
			{
				nextData();
				runNextTask();
			}
		}
				
		override public function prepare():void
		{
			_currentDataIndex = -1;
			super.prepare();
		}

		override public function cleanUp():void
		{
			_dataProvider = null;
			super.cleanUp();
		}
		
	}
}